using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing.Drawing2D;
using System.Drawing;

namespace Woz
{
    /// <summary>
    /// represents a transition line between two screen thumbnails
    /// </summary>
    public class TransitionArrow : IDisposable
    {
        /// <summary>
        /// initializes the static variables of the transition line
        /// </summary>
        static TransitionArrow()
        {
            // define the color of the transition
            Color transitionPenColor = Color.FromArgb(34, 179, 34);
            
            // initialize the transition pen
            sTransitionPen = Woz.ResourceManager.Instance.GetPen(transitionPenColor, 8.0f);
            sTransitionPen.EndCap = LineCap.ArrowAnchor;
            sTransitionPen.DashCap = DashCap.Triangle;
            sTransitionPen.StartCap = LineCap.RoundAnchor;

            // initialize the transition outline pen
            sTransitionPenOutline = Woz.ResourceManager.Instance.GetPen(Color.Green, 10.0f);
            sTransitionPenOutline.EndCap = LineCap.ArrowAnchor;
            sTransitionPenOutline.DashCap = DashCap.Triangle;
            sTransitionPenOutline.StartCap = LineCap.RoundAnchor;

            // initialize the selected transition pen
            sTransitionSelectedPen = Woz.ResourceManager.Instance.GetPen(Color.FromArgb(249, 234, 0), 9.0f);
            sTransitionSelectedPen.EndCap = LineCap.ArrowAnchor;
            sTransitionSelectedPen.DashCap = DashCap.Triangle;
            sTransitionSelectedPen.StartCap = LineCap.RoundAnchor;

            // initialize the stretched transition pen
            sTransitionStretchedPen = Woz.ResourceManager.Instance.GetPen(Color.FromArgb(20, transitionPenColor), 10.0f);
            sTransitionStretchedPen.EndCap = LineCap.ArrowAnchor;
            sTransitionStretchedPen.DashCap = DashCap.Triangle;
            sTransitionStretchedPen.StartCap = LineCap.RoundAnchor;
        }

        /// <summary>
        /// creates a transition line between two screen thumbnails
        /// </summary>
        /// <param name="pStartThumbnail">the screen thumbnail the transition line starts at</param>
        /// <param name="pEndThumbnail">the screen thumbnail the transition line ends at</param>
        public TransitionArrow(Woz.ScreenThumbnail pStartThumbnail, Woz.ScreenThumbnail pEndThumbnail)
        {
            // initialize the transition line path
            this.mTransitionPath = new GraphicsPath();

            // initialize stretched to far to false
            this.mStretchedTooFar = false;

            // save the start thumbnail
            this.mStartThumbnail = pStartThumbnail;

            // save the end thumbnail
            this.mEndThumbnail = pEndThumbnail;

            // subscribe to the location changed event of either
            this.mStartThumbnail.LocationChanged += this.cScreenThumbnailHasMoved;
            this.mEndThumbnail.LocationChanged += this.cScreenThumbnailHasMoved;

            // initialize the transition points
            this.cScreenThumbnailHasMoved(null, EventArgs.Empty);
        }

        /// <summary>
        /// gets the bounding box of this transition line
        /// </summary>
        public Rectangle BoundingBox
        {
            get
            {
                return this.mBoundingBox;
            }
        }

        /// <summary>
        /// gets the start thumbnail of the transition
        /// </summary>
        public Woz.ScreenThumbnail StartThumbnail
        {
            get
            {
                return this.mStartThumbnail;
            }
        }

        /// <summary>
        /// gets the end thumbnail of the transition
        /// </summary>
        public Woz.ScreenThumbnail EndThumbnail
        {
            get
            {
                return this.mEndThumbnail;
            }
        }

        /// <summary>
        /// gets whether this transition has been stretched too far
        /// </summary>
        public bool StretchedTooFar
        {
            get
            {
                return this.mStretchedTooFar;
            }
        }

        /// <summary>
        /// gets or sets whether the mouse is over this transition
        /// </summary>
        public bool MouseOver
        {
            get
            {
                return this.mMouseOver;
            }
            set
            {
                this.mMouseOver = value;
            }
        }

        /// <summary>
        /// gets whether this transition is selected
        /// </summary>
        public bool Selected
        {
            get
            {
                return this.mSelected;
            }
        }

        /// <summary>
        /// gets the stretch position
        /// </summary>
        internal Point StretchPosition
        {
            get
            {
                return this.mTransitionArrow[1];
            }
        }

        /// <summary>
        /// releases all memory associatied with this transition
        /// </summary>
        public void Dispose()
        {
            // unsubscribe from the start and end thumbnails location changed events
            this.mStartThumbnail.LocationChanged -= this.cScreenThumbnailHasMoved;
            this.mEndThumbnail.LocationChanged -= this.cScreenThumbnailHasMoved;
        }

        /// <summary>
        /// draw the transition to the given graphics object
        /// </summary>
        /// <param name="pGraphics">the graphics object to draw the transition to</param>
        public void Draw(Graphics pGraphics)
        {
            // draw a semi transparent version of the transition if we stretched too far
            if (this.mStretchedTooFar)
            {
                pGraphics.DrawPath(Woz.TransitionArrow.sTransitionStretchedPen, this.mTransitionPath);
            }

            // otherwise draw a normal transition
            else
            {
                // first draw the transition line curve
                if (this.mSelected)
                {
                    pGraphics.DrawPath(Woz.TransitionArrow.sTransitionSelectedPen, this.mTransitionPath);
                }
                else
                {
                    // draw the shadow
                    pGraphics.DrawCurve(Woz.TransitionArrow.sTransitionPenOutline, this.mTransitionArrow, 0.5f);

                    // draw the transition
                    pGraphics.DrawCurve(Woz.TransitionArrow.sTransitionPen, this.mTransitionArrowShadow, 0.5f);
                }

                // then draw the middle point
                if (this.mMouseOver)
                {
                    pGraphics.DrawEllipse(Pens.Green,
                        Rectangle.FromLTRB(
                            this.mTransitionArrow[1].X - 8,
                            this.mTransitionArrow[1].Y - 8,
                            this.mTransitionArrow[1].X + 8,
                            this.mTransitionArrow[1].Y + 8));
                }
            }
        }

        /// <summary>
        /// selects the transition line
        /// </summary>
        public void Select()
        {
            this.mSelected = true;
        }

        /// <summary>
        /// deselects the transition line
        /// </summary>
        public void Deselect()
        {
            this.mSelected = false;
        }

        /// <summary>
        /// stretches the transition line by moving the middle of the transition 
        /// line to the given strecth position
        /// </summary>
        /// <param name="pStrecthPosition">the position to move the middle of the transition line to</param>
        public void StrecthTo(Point pStretchPosition)
        {
            // get the stretch difference
            Point stretchDifference = new Point(
                pStretchPosition.X - this.mTransitionArrow[1].X,
                pStretchPosition.Y - this.mTransitionArrow[1].Y);

            // update the middle position of the transition arrow
            this.mTransitionArrow[1].X += stretchDifference.X;
            this.mTransitionArrow[1].Y += stretchDifference.Y;

            // update the middle position of the transition arrow shadow
            this.mTransitionArrowShadow[1].X += stretchDifference.X;
            this.mTransitionArrowShadow[1].Y += stretchDifference.Y;

            // get the distance between the middle point and the stretch position
            double distance = CalculateLinePointDistance(this.mTransitionArrow[0],
                this.mTransitionArrow[2], pStretchPosition);

            // check if we stretched too far
            this.mStretchedTooFar = (distance > Woz.StaticVariables.Instance.MaximumStretchTension);

            // reset the path and bounding box
            this.mTransitionPath.Reset();
            this.mTransitionPath.AddCurve(this.mTransitionArrow);
            RectangleF rf = this.mTransitionPath.GetBounds();
            this.mBoundingBox = new Rectangle((int)rf.X, (int)rf.Y, (int)rf.Width, (int)rf.Height);
            this.mBoundingBox.Inflate(10, 10);
        }

        /// <summary>
        /// checks to see if the given point intersects with this transition line
        /// </summary>
        /// <param name="pPoint">the point to check for</param>
        /// <returns>true if the point is on the transition line false otherwise</returns>
        public bool HitTest(Point pPoint)
        {
            return this.mTransitionPath.IsOutlineVisible(pPoint, sTransitionPen);
        }

        /// <summary>
        /// checks to see if this transition is connected to the given thumbnail
        /// </summary>
        /// <param name="pThumbnail">the thumbnail to check for</param>
        /// <returns>true if this transition is connected to the thumbnail false otherwise</returns>
        public bool ConnectedToThumbnail(Woz.ScreenThumbnail pThumbnail)
        {
            return (this.mEndThumbnail == pThumbnail) || (this.mStartThumbnail == pThumbnail);
        }

        /// <summary>
        /// draws a transition line between two rectangles
        /// </summary>
        /// <param name="pGraphics">the graphics object to draw on</param>
        /// <param name="pStart">the starting rectangle to draw the transition arrow from</param>
        /// <param name="pEnd">the ending rectangle to draw the transition arrow to</param>
        public static void DrawTransition(Graphics pGraphics, Rectangle pStart, Rectangle pEnd)
        {
            // get all the transition points
            Point[] allTransitionPoints = ComputeTransition(pStart, pEnd);

            // copy just the curve points which are the first three points
            Point[] curveTransitionPoints = new Point[3];
            Array.Copy(allTransitionPoints, curveTransitionPoints, 3);

            // draw the curve using the curve points
            pGraphics.DrawCurve(sTransitionPen, curveTransitionPoints, 0.5f);
        }

        /// <summary>
        /// computes a transition line between two rectangles and returns the points that
        /// make up that transition line
        /// </summary>
        /// <param name="pStartRect">the starting rectangle</param>
        /// <param name="pEndRect">the ending rectangle</param>
        /// <returns>an array of three points starting point middle point and ending point</returns>
        private static Point[] ComputeTransition(Rectangle pStartRect, Rectangle pEndRect)
        {
            Point start, end, middle;
            Point shadowOffset, staggerStart, staggerEnd;

            // initialize the start middle and end
            start = new Point();
            end = new Point();
            middle = new Point();

            // initialize the shadow offset stagger start and stagger end
            shadowOffset = new Point();
            staggerStart = new Point();
            staggerEnd = new Point();

            // initialize the curve steepness
            int curveSteepness = 5;

            // initialize the top offset
            int topOffset = 18;

            // initialize the shadow offset amount
            int shadowOffsetAmount = 2;

            // given the following configuration:
            //  1  2  3
            //  4  5  6
            //  7  8  9
            // we will compute the curves between 5 and any other number 
            // where 5 is the starting rectangle
            if ((pEndRect.Right < pStartRect.Left) && (pEndRect.Bottom < pStartRect.Top))
            {
                // 1
                start.X = pStartRect.Left;
                start.Y = pStartRect.Top + pStartRect.Height / 2;

                end.X = pEndRect.Left + pEndRect.Width / 2;
                end.Y = pEndRect.Bottom;

                middle.X = ((end.X + start.X) / 2) - Math.Abs((end.X - start.X) / curveSteepness);
                middle.Y = ((end.Y + start.Y) / 2) + Math.Abs((end.Y - start.Y) / curveSteepness);

                shadowOffset.Y = shadowOffsetAmount;
            }
            else if ((pEndRect.Left > pStartRect.Right) && (pEndRect.Bottom < pStartRect.Top))
            {
                // 3
                start.X = pStartRect.Right;
                start.Y = pStartRect.Top + pStartRect.Height / 2;

                end.X = pEndRect.Left + pEndRect.Width / 2;
                end.Y = pEndRect.Bottom;

                middle.X = ((end.X + start.X) / 2) + Math.Abs((end.X - start.X) / curveSteepness);
                middle.Y = ((end.Y + start.Y) / 2) + Math.Abs((end.Y - start.Y) / curveSteepness);

                shadowOffset.Y = shadowOffsetAmount;
            }
            else if ((pEndRect.Right < pStartRect.Left) && (pEndRect.Top > pStartRect.Bottom))
            {
                // 7
                start.X = pStartRect.Left;
                start.Y = pStartRect.Top + pStartRect.Height / 2;

                end.X = pEndRect.Left + pEndRect.Width / 2;
                end.Y = pEndRect.Top - topOffset;

                middle.X = ((end.X + start.X) / 2) - Math.Abs((end.X - start.X) / curveSteepness);
                middle.Y = ((end.Y + start.Y) / 2) - Math.Abs((end.Y - start.Y) / curveSteepness);

                shadowOffset.Y = -shadowOffsetAmount;
            }
            else if ((pEndRect.Left > pStartRect.Right) && (pEndRect.Top > pStartRect.Bottom))
            {
                // 9
                start.X = pStartRect.Right;
                start.Y = pStartRect.Top + pStartRect.Height / 2;

                end.X = pEndRect.Left + pEndRect.Width / 2;
                end.Y = pEndRect.Top - topOffset;

                middle.X = ((end.X + start.X) / 2) + Math.Abs((end.X - start.X) / curveSteepness);
                middle.Y = ((end.Y + start.Y) / 2) - Math.Abs((end.Y - start.Y) / curveSteepness);

                shadowOffset.Y = -shadowOffsetAmount;
            }
            else if (pEndRect.Bottom < pStartRect.Top)
            {
                // 2
                start.X = pStartRect.Left + pStartRect.Width / 2;
                start.Y = pStartRect.Top - topOffset;

                end.X = pEndRect.Left + pEndRect.Width / 2;
                end.Y = pEndRect.Bottom;

                middle.X = ((end.X + start.X) / 2);
                middle.Y = ((end.Y + start.Y) / 2);

                shadowOffset.Y = shadowOffsetAmount;
            }
            else if (pEndRect.Top > pStartRect.Bottom)
            {
                // 8
                start.X = pStartRect.Left + pStartRect.Width / 2;
                start.Y = pStartRect.Bottom;

                end.X = pEndRect.Left + pEndRect.Width / 2;
                end.Y = pEndRect.Top - topOffset;

                middle.X = ((end.X + start.X) / 2);
                middle.Y = ((end.Y + start.Y) / 2);

                shadowOffset.Y = -shadowOffsetAmount;
            }
            else if (pEndRect.Right < pStartRect.Left)
            {
                // 4
                start.X = pStartRect.Left;
                start.Y = pStartRect.Top + pStartRect.Height / 2;

                end.X = pEndRect.Right;
                end.Y = pEndRect.Top + pEndRect.Height / 2;

                middle.X = ((end.X + start.X) / 2);
                middle.Y = ((end.Y + start.Y) / 2);

                shadowOffset.X = shadowOffsetAmount;
            }
            else //if (pEndRect.Left > pStartRect.Right)
            {
                // 6
                start.X = pStartRect.Right;
                start.Y = pStartRect.Top + pStartRect.Height / 2;

                end.X = pEndRect.Left;
                end.Y = pEndRect.Top + pEndRect.Height / 2;

                middle.X = ((end.X + start.X) / 2);
                middle.Y = ((end.Y + start.Y) / 2);

                shadowOffset.X = -shadowOffsetAmount;
            }

            // return the start middle and end points of the curve 
            // as well as the shadow offset stagger start and stagger end points
            return new Point[] { start, middle, end, shadowOffset, staggerStart, staggerEnd };
        }

        /// <summary>
        /// calculates the distance between two points in a line and returns it
        /// </summary>
        /// <param name="pInitialPoint">the initial point</param>
        /// <param name="pFinalPoint">the final point</param>
        /// <returns>the distance between the initial point and the final point</returns>
        public static double CalculateLineDistance(Point pInitialPoint, Point pFinalPoint)
        {
            // get the x and y difference
            int xDiff = pFinalPoint.X - pInitialPoint.X;
            int yDiff = pFinalPoint.Y - pInitialPoint.Y;
            
            // calculate the distance between them
            return Math.Sqrt((xDiff * xDiff) + (yDiff * yDiff));
        }
        
        private static int CalculateDotProduct(Point pA, Point pB, Point pC)
        {
            return ((pB.X - pA.X) * (pC.X - pB.X)) + ((pB.Y - pA.Y) * (pC.Y - pB.Y));
        }

        private static int CalculateCrossProduct(Point pA, Point pB, Point pC)
        {
            return ((pB.X - pA.X) * (pC.Y - pA.Y)) - ((pB.Y - pA.Y) * (pC.X - pA.X));
        }

        private static double CalculateLinePointDistance(Point pA, Point pB, Point pC)
        {
            double dist = CalculateCrossProduct(pA, pB, pC) / CalculateLineDistance(pA, pB);
            int dot1 = CalculateDotProduct(pA, pB, pC);
            if (dot1 > 0) return CalculateLineDistance(pB, pC);
            int dot2 = CalculateDotProduct(pB, pA, pC);
            if (dot2 > 0) return CalculateLineDistance(pA, pC);
            return Math.Abs(dist);
        }

        /// <summary>
        /// handles when any screen thumbnail has moved
        /// </summary>
        /// <param name="pSender">the sender of the event</param>
        /// <param name="pEventArgs">the event args associated with this event</param>
        private void cScreenThumbnailHasMoved(object pSender, EventArgs pEventArgs)
        {
            // get all the transition points
            Point[] allTransitionPoints = ComputeTransition(this.mStartThumbnail.Bounds, this.mEndThumbnail.Bounds);

            // create and initialize the transition arrows and transition arrow shadows
            this.mTransitionArrow = new Point[3];
            this.mTransitionArrowShadow = new Point[3];
            for (int i = 0; i < 3; i++)
            {
                // get the transition arrow position
                this.mTransitionArrow[i] = allTransitionPoints[i];

                // also get the shadow position
                this.mTransitionArrowShadow[i] = new Point(
                    allTransitionPoints[i].X + allTransitionPoints[3].X,
                    allTransitionPoints[i].Y + allTransitionPoints[3].Y);
            }

            // do the staggering here
            {
            }

            // find the middle point
            this.mMiddlePoint = new Point((this.mTransitionArrow[2].X + this.mTransitionArrow[0].X) / 2,
                (this.mTransitionArrow[2].Y + this.mTransitionArrow[0].Y) / 2);

            // reset the path and bounding box
            this.mTransitionPath.Reset();
            this.mTransitionPath.AddCurve(this.mTransitionArrow);
            RectangleF rf = this.mTransitionPath.GetBounds();
            this.mBoundingBox = new Rectangle((int)rf.X, (int)rf.Y, (int)rf.Width, (int)rf.Height);
            this.mBoundingBox.Inflate(10, 10);

            // invalidate the transition panel
            if (this.mStartThumbnail.Parent != null)
            {
                Rectangle invalidateRect = Rectangle.Union(this.mBoundingBox,
                    Rectangle.Union(this.mStartThumbnail.Bounds, this.mEndThumbnail.Bounds));
                invalidateRect.Inflate(10, 20);
                this.mStartThumbnail.Parent.Invalidate(invalidateRect);
            }
        }

        /// <summary>
        /// the thumbnail to start drawing transition from
        /// </summary>
        private Woz.ScreenThumbnail mStartThumbnail;

        /// <summary>
        /// the thumbnail to draw a transition to
        /// </summary>
        private Woz.ScreenThumbnail mEndThumbnail;

        /// <summary>
        /// graphics path to help figure out the hittest
        /// </summary>
        private GraphicsPath mTransitionPath;

        /// <summary>
        /// the three points of the transition arrow
        /// </summary>
        private Point[] mTransitionArrow;

        /// <summary>
        /// the shadow offset of the transition arrow
        /// </summary>
        private Point[] mTransitionArrowShadow;

        /// <summary>
        /// the absolute middle point of the transition line
        /// </summary>
        private Point mMiddlePoint;

        /// <summary>
        /// the bounding box of the transition line
        /// </summary>
        private Rectangle mBoundingBox;

        /// <summary>
        /// specifies whether or not the transition line has been stretched too far
        /// </summary>
        private bool mStretchedTooFar;

        /// <summary>
        /// specifies whether or not the transition line has the mouse over it
        /// </summary>
        private bool mMouseOver;

        /// <summary>
        /// specifies whether or not the transition line is selected
        /// </summary>
        private bool mSelected;

        /// <summary>
        /// the standard transition arrow pen
        /// </summary>
        private static Pen sTransitionPen;

        /// <summary>
        /// the standard transition arrow pen outline
        /// </summary>
        private static Pen sTransitionPenOutline;

        /// <summary>
        /// the stretched transition arrow pen
        /// </summary>
        private static Pen sTransitionStretchedPen;

        /// <summary>
        /// the selected transition arrow pen
        /// </summary>
        private static Pen sTransitionSelectedPen;
    }
}
